package com.ctrip.corp.obt.shard.configuration;


import com.ctrip.corp.obt.generic.etcd.EtcdConfigFactory;
import com.ctrip.corp.obt.generic.etcd.EtcdProperties;
import com.ctrip.corp.obt.generic.etcd.IConfigCenter;
import com.ctrip.corp.obt.platform.annotation.EnablePlatformApi;
import com.ctrip.corp.obt.shard.annotation.ShardModelAnnotationBeanPostProcessor;
import com.ctrip.corp.obt.shard.aop.page.PageResponseAdvice;
import com.ctrip.corp.obt.shard.aop.shard.ShardInfoDSAspect;
import com.ctrip.corp.obt.shard.aop.shard.ShardModelDSAspect;
import com.ctrip.corp.obt.shard.controller.DataPlaneSourceController;
import com.ctrip.corp.obt.shard.core.ShardDataSource;
import com.ctrip.corp.obt.shard.datasource.DynamicRoutingDataSource;
import com.ctrip.corp.obt.shard.domain.gateway.DataShardSourceGateway;
import com.ctrip.corp.obt.shard.filter.PageFilter;
import com.ctrip.corp.obt.shard.handler.DefaultShardingHandler;
import com.ctrip.corp.obt.shard.handler.IShardingHandler;
import com.ctrip.corp.obt.shard.handler.strategy.DateTableRouteStrategy;
import com.ctrip.corp.obt.shard.handler.strategy.DefaultTableRouteStrategy;
import com.ctrip.corp.obt.shard.handler.strategy.HashTableRouteStrategy;
import com.ctrip.corp.obt.shard.infrastructure.AggregateDataShardSourceGateway;
import com.ctrip.corp.obt.shard.infrastructure.apollo.ApolloClient;
import com.ctrip.corp.obt.shard.infrastructure.apollo.ApolloDataShardSourceGateway;
import com.ctrip.corp.obt.shard.infrastructure.etcd.EtcdDataShardSourceGateway;
import com.ctrip.corp.obt.shard.infrastructure.etcd.ShardSourceConfigurator;
import com.ctrip.corp.obt.shard.infrastructure.local.LocalDataShardSourceGateway;
import com.ctrip.corp.obt.shard.infrastructure.mapper.MybatisDataShardSourceGateway;
import com.ctrip.corp.obt.shard.loader.IShardSourceLoader;
import com.ctrip.corp.obt.shard.loader.factory.ShardSourceLoaderFactory;
import com.ctrip.corp.obt.shard.manager.DataPlaneSourceManager;
import com.ctrip.corp.obt.shard.manager.DataShardSourceManager;
import com.ctrip.corp.obt.shard.sql.mybatis.OverrideConfiguration;
import com.ctrip.corp.obt.shard.sql.mybatis.OverrideSqlSessionFactoryBean;
import com.google.common.collect.Maps;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ObjectUtils;

import javax.sql.DataSource;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;

import static com.ctrip.corp.obt.generic.constants.GenericConstants.DEFAULT;

@Slf4j
@Configuration
@RequiredArgsConstructor
@MapperScan({"com.ctrip.corp.obt.shard.infrastructure.mapper"})
@EnableConfigurationProperties({DataShardProperties.class, MybatisProperties.class, DruidDatabaseProperties.class})
@EnablePlatformApi
public class DataShardAutoConfiguration {

    private final DataShardProperties dataShardProperties;
    private final MybatisProperties mybatisProperties;
    private final DruidDatabaseProperties dbProperties;
    private final ResourceLoader resourceLoader;

    @Bean
    @ConditionalOnProperty(value = "arch.shard.source.apollo.enable", havingValue = "true", matchIfMissing = true)
    public DataShardSourceGateway apolloDataShardSourceClient() {
        return new ApolloDataShardSourceGateway(new ApolloClient());
    }

    @Bean
    @ConditionalOnProperty(value = "arch.shard.source.etcd.enable", havingValue = "true")
    public DataShardSourceGateway etcdDataShardSourceGateway(EtcdProperties etcdProperties) {
        IConfigCenter configCenter = EtcdConfigFactory.buildConfigCenter(etcdProperties.getEtcdEndpoints());
        return new EtcdDataShardSourceGateway(configCenter);
    }

    /**
     * 使用apollo来配置数据源（适用于没接入etcd的本地化客户）
     * @return
     */
    @Bean
    @ConditionalOnProperty(value = "arch.shard.source.local.enable", havingValue = "true", matchIfMissing = true)
    public DataShardSourceGateway localDataShardSourceGateway() {
        return new LocalDataShardSourceGateway(dataShardProperties);
    }

    @Bean
    @ConditionalOnProperty(value = "arch.shard.source.mybatis.enable", havingValue = "true")
    public DataShardSourceGateway mybatisDataShardSourceGateway() {
        return new MybatisDataShardSourceGateway();
    }

    @Bean("aggregateDataShardSourceGateway")
    public DataShardSourceGateway dataShardSourceGateway(List<DataShardSourceGateway> dataShardSourceGatewayList) {
        return new AggregateDataShardSourceGateway(dataShardSourceGatewayList);
    }

    @Bean("etcdShardSourceWatcher")
    public ShardSourceConfigurator etcdShardSourceWatcher(IShardSourceLoader shardSourceLoader) {
        return new ShardSourceConfigurator(shardSourceLoader);
    }

    /**
     * 如果不想使用此处配置的默认的DataSource，请自行配置DataSource并添加注解 @Bean(name = {DataShardProperties.DEFAULT_DATA_SOURCE})
     */
    @Primary
    @ConditionalOnMissingBean(value = DataSource.class)
    @Bean
    public DataSource defaultDataSource() {
        System.setProperty("druid.mysql.usePingMethod", "false");
        return ShardDataSource.buildDefaultDatasource(dbProperties);
    }


    @Bean(name = "dynamicRoutingDataSource")
    @ConditionalOnMissingBean(value = DynamicRoutingDataSource.class)
    public DynamicRoutingDataSource dynamicRoutingDataSource(DataSource dataSource) {
        Map<Object, Object> dataSourceMap = Maps.newHashMap();
        dataSourceMap.put(DEFAULT, dataSource);
        return DynamicRoutingDataSource.create(dataSource, dataSourceMap);
    }

    @Primary
    @Bean({"sqlSessionFactory"})
    public SqlSessionFactory getSqlSessionFactory(
            @Qualifier("dynamicRoutingDataSource") DataSource dynamicDataSource,
            Interceptor[] interceptors,
            DatabaseIdProvider databaseIdProvider
    ) throws Exception {
        log.info("Start sqlSessionFactory loading");
        OverrideSqlSessionFactoryBean factory = new OverrideSqlSessionFactoryBean();
        factory.setDataSource(dynamicDataSource);
        setConfiguration(factory, databaseIdProvider);
        //解决 application.properties 中 配置mybatis.mapper-locations 失效的问题
        if (!ObjectUtils.isEmpty(this.mybatisProperties.resolveMapperLocations())) {
            factory.setMapperLocations(this.mybatisProperties.resolveMapperLocations());
        }
        Optional.ofNullable(this.mybatisProperties.getTypeAliasesPackage()).ifPresent(factory::setTypeAliasesPackage);
        Optional.ofNullable(this.mybatisProperties.getTypeHandlersPackage()).ifPresent(factory::setTypeHandlersPackage);
        Optional.ofNullable(factory.getObject())
                .map(SqlSessionFactory::getConfiguration)
                .ifPresent(configuration -> configuration.setMapUnderscoreToCamelCase(true));
        if (null != interceptors && interceptors.length > 0) {
            for (Interceptor interceptor : interceptors) {
                Optional.ofNullable(factory.getObject())
                        .map(SqlSessionFactory::getConfiguration)
                        .ifPresent(configuration -> configuration.addInterceptor(interceptor));
            }
        }
        return factory.getObject();
    }

    private void setConfiguration(OverrideSqlSessionFactoryBean factory, DatabaseIdProvider databaseIdProvider) {
        String configLocation = Optional.ofNullable(this.mybatisProperties.getConfigLocation()).orElse("classpath:mybatis-config.xml");
        // 如果用户自定义了configLocation或者classpath:mybatis-config.xml文件存在，则使用加载的文件
        Resource resource = this.resourceLoader.getResource(configLocation);
        if (resource.isReadable()) {
            factory.setConfigLocation(resource);
            log.info("Loading mybatis config: {}", configLocation);
        } else {
            // 如果不存在上述文件，则使用默认的Configuration
            factory.setConfiguration(new OverrideConfiguration());
        }
        factory.setDatabaseIdProvider(databaseIdProvider);
    }

    @Primary
    @Bean
    public PlatformTransactionManager transactionManager(@Qualifier("dynamicRoutingDataSource") DataSource dynamicDataSource) {
        log.info("Start transactionManager loading");
        return new DataSourceTransactionManager(dynamicDataSource);
    }

    @Bean
    public DataShardSourceManager dataShardSourceManager(
            @Qualifier("aggregateDataShardSourceGateway") DataShardSourceGateway dataShardSourceGateway,
            DynamicRoutingDataSource routingDataSource,
            DataSource dataSource
    ) {
        return new DataShardSourceManager(dataShardSourceGateway, routingDataSource, dataSource);
    }

    @Bean
    public DataPlaneSourceManager dataPlaneSourceManager(IShardSourceLoader shardSourceLoader) {
        return new DataPlaneSourceManager(shardSourceLoader);
    }

    @Bean
    public IShardSourceLoader shardSourceLoader(DataShardSourceManager dataShardSourceManager) {
        ShardSourceLoaderFactory factory = new ShardSourceLoaderFactory(dataShardSourceManager);
        return factory.create(dataShardProperties.getSourceLoadMode());
    }

    @Bean
    public ShardModelAnnotationBeanPostProcessor shardModelAnnotationBeanPostProcessor(IShardingHandler shardingHandler) {
        return new ShardModelAnnotationBeanPostProcessor(shardingHandler);
    }

    @Bean
    public PageFilter pageFilter() {
        return new PageFilter();
    }

    @Bean
    @ConditionalOnMissingBean(IShardingHandler.class)
    public IShardingHandler shardingHandler() {
        return new DefaultShardingHandler(Arrays.asList(
                new DateTableRouteStrategy(),
                new DefaultTableRouteStrategy(),
                new HashTableRouteStrategy(dataShardProperties)));
    }

    // ------------------------------------------------Aop 注入------------------------------------------------------------------

    @Bean
    public ShardModelDSAspect shardModelDSAspect(IShardingHandler shardingHandler) {
        return new ShardModelDSAspect(shardingHandler);
    }

    @Bean
    public ShardInfoDSAspect shardInfoDSAspect(IShardingHandler shardingHandler) {
        return new ShardInfoDSAspect(shardingHandler, dataShardProperties);
    }

    @Bean
    public PageResponseAdvice myResponseAdvice() {
        return new PageResponseAdvice();
    }

    // ------------------------------------------------端点暴露------------------------------------------------------------------

    @Bean
    public DataPlaneSourceController dataPlaneSourceController(DataPlaneSourceManager dataPlaneSourceManager) {
        return new DataPlaneSourceController(dataPlaneSourceManager);
    }

    // ------------------------------------------------数据库方言配置------------------------------------------------------------------
    @Bean
    public DatabaseIdProvider getDatabaseIdProvider() {
        VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        Properties properties = new Properties();
        properties.setProperty("Oracle", "oracle");
        properties.setProperty("MySQL", "mysql");
        properties.setProperty("PostgreSQL", "postgresql");
        properties.setProperty("SQL Server", "sqlserver");
        databaseIdProvider.setProperties(properties);
        return databaseIdProvider;
    }

}